Weekend Business Activity Zones in Melbourne

Authored by: SAHITHI PINNAM

Duration: 90 mins

Level: Intermediate

Pre-requisite Skills: Python, Pandas, NumPy, Matplotlib, Folium (basic geospatial)

Scenario

As a business owner or city economic planner in Melbourne, I want to identify where weekend pedestrian activity is highest and where businesses are concentrated, so I can plan opening hours, promotions, staffing, and events to match weekend demand.

As a planner/analyst, I want named examples of nearby hospitality venues (cafés/restaurants) around the busiest zones to ground decisions in real places.

What this use case will teach you

By the end of this use case you will have:

Pulled multiple City of Melbourne datasets with the API v2.1. Cleaned, filtered, and merged business + pedestrian data for weekend analysis. Performed distance-based proximity joins (≤200 m). Built a bar chart of top weekend hotspots and an interactive map with business counts and named cafés. Written business-oriented insights and recommendations.

Introduction

Weekend activity in Melbourne is driven by retail, hospitality, leisure, and events. This project focuses on where people go on weekends and what businesses are nearby. We combine pedestrian sensors (for weekend footfall) with business locations and a named cafés dataset to deliver actionable guidance for Business & Economy stakeholders.

Datasets used

Business Establishments with Address and Industry Classification [https://data.melbourne.vic.gov.au/explore/dataset/business-establishments-with-address-and-industry-classification/information/?disjunctive.industry_anzsic4_description&disjunctive.block_id&disjunctive.clue_small_area&disjunctive.industry_anzsic4_code] Identifier: business-establishments-with-address-and-industry-classification Use: overall business density near hotspots (counts within 200 m).

Cafes and Restaurants with Seating Capacity[https://data.melbourne.vic.gov.au/explore/dataset/cafes-and-restaurants-with-seating-capacity/information/?disjunctive.block_id&disjunctive.industry_anzsic4_code&disjunctive.industry_anzsic4_description&disjunctive.number_of_seats&disjunctive.clue_small_area] Identifier: cafes-and-restaurants-with-seating-capacity Use: named venues (examples) near hotspots to enrich business context.

Pedestrian Counting System – Monthly counts per hour[https://data.melbourne.vic.gov.au/explore/?q=1.%09Pedestrian+Counting+System+–+Monthly+counts+per+hour&sort=modified] Identifier: pedestrian-counting-system-monthly-counts-per-hour Use: hourly counts; filter Saturday/Sunday and aggregate by sensor.

Pedestrian Counting System – Sensor locations[https://data.melbourne.vic.gov.au/explore/dataset/pedestrian-counting-system-sensor-locations/information/] Identifier: pedestrian-counting-system-sensor-locations Use: coordinates for each sensor to map and compute distances.

Importing Datasets

This section imports libraries for data manipulation, visualisation, simple geospatial operations, and API access.

In [1]:
# Libraries
import pandas as pd
import numpy as np
import requests, io, os
import matplotlib.pyplot as plt
import folium

pd.set_option("display.max_colwidth", 200)

Loading the datasets using API v2.1

We define a helper to fetch full CSV exports from the City of Melbourne API v2.1.

In [2]:
def fetch_dataset_csv(base_url, dataset, api_key=None):
    suffix = 'exports/csv?delimiter=%3B&list_separator=%2C&quote_all=false&with_bom=true'
    url = f"{base_url}{dataset}/{suffix}"
    params = {'api_key': api_key} if api_key else {}
    r = requests.get(url, params=params, timeout=30)
    r.raise_for_status()
    return pd.read_csv(io.BytesIO(r.content), delimiter=';')

Fetching and Previewing Datasets

We’ll load the four datasets and quickly preview shapes and columns.

In [3]:
API_KEY = os.environ.get('MELBOURNE_API_KEY', '')
BASE_URL = 'https://data.melbourne.vic.gov.au/api/explore/v2.1/catalog/datasets/'

DATASETS = {
    "business": "business-establishments-with-address-and-industry-classification",
    "cafes": "cafes-and-restaurants-with-seating-capacity",
    "pedestrian": "pedestrian-counting-system-monthly-counts-per-hour",
    "locations": "pedestrian-counting-system-sensor-locations"
}


business_df  = fetch_dataset_csv(BASE_URL, DATASETS["business"], API_KEY)
cafes_df     = fetch_dataset_csv(BASE_URL, DATASETS["cafes"], API_KEY)
ped_df       = fetch_dataset_csv(BASE_URL, DATASETS["pedestrian"], API_KEY)
loc_df       = fetch_dataset_csv(BASE_URL, DATASETS["locations"], API_KEY)

print("Business columns:", business_df.columns.tolist()[:12], "... | rows:", len(business_df))
print("Cafes columns   :", cafes_df.columns.tolist()[:12],    "... | rows:", len(cafes_df))
print("Ped columns     :", ped_df.columns.tolist()[:12],      "... | rows:", len(ped_df))
print("Loc columns     :", loc_df.columns.tolist()[:12],      "... | rows:", len(loc_df))
Business columns: ['census_year', 'block_id', 'property_id', 'base_property_id', 'clue_small_area', 'trading_name', 'business_address', 'industry_anzsic4_code', 'industry_anzsic4_description', 'longitude', 'latitude', 'point'] ... | rows: 393878
Cafes columns   : ['census_year', 'block_id', 'property_id', 'base_property_id', 'building_address', 'clue_small_area', 'trading_name', 'business_address', 'industry_anzsic4_code', 'industry_anzsic4_description', 'seating_type', 'number_of_seats'] ... | rows: 63121
Ped columns     : ['id', 'location_id', 'sensing_date', 'hourday', 'direction_1', 'direction_2', 'pedestriancount', 'sensor_name', 'location'] ... | rows: 1405267
Loc columns     : ['location_id', 'sensor_description', 'sensor_name', 'installation_date', 'note', 'location_type', 'status', 'direction_1', 'direction_2', 'latitude', 'longitude', 'location'] ... | rows: 143

Displaying Dataset Overview

We focus on columns needed for business-first analysis: names (for cafés), coordinates, and pedestrian counts.

In [4]:
business_df = business_df.copy()
cafes_df    = cafes_df.copy()
ped_df      = ped_df.copy()
loc_df      = loc_df.copy()

# Standardise coordinates to numeric
for df in (business_df, cafes_df, loc_df):
    if 'latitude' in df.columns:
        df['latitude']  = pd.to_numeric(df['latitude'], errors='coerce')
    if 'longitude' in df.columns:
        df['longitude'] = pd.to_numeric(df['longitude'], errors='coerce')

# Cafes: normalise name column if present
name_cols = [c for c in cafes_df.columns if c.lower() in ['trading_name','business_name','name','organisation']]
if name_cols:
    cafes_df.rename(columns={name_cols[0]: 'business_name'}, inplace=True)
else:
    cafes_df['business_name'] = "Unnamed Café/Restaurant"

# Drop rows without coordinates
business_df = business_df.dropna(subset=['latitude','longitude']).copy()
cafes_df    = cafes_df.dropna(subset=['latitude','longitude']).copy()
loc_df      = loc_df.dropna(subset=['latitude','longitude']).copy()

business_df.head(3), cafes_df[['business_name','latitude','longitude']].head(3), loc_df.head(3)
Out[4]:
(   census_year  block_id  property_id  base_property_id clue_small_area  \
 0         2010      1101       110843            110843       Docklands   
 1         2010      1101       110843            110843       Docklands   
 2         2010      1101       110843            110843       Docklands   
 
         trading_name  \
 0             Vacant   
 1         Newsxpress   
 2  Lifestyle Luggage   
 
                                            business_address  \
 0                     163-235 Spencer Street DOCKLANDS 3008   
 1  Shop 302, Ground , 237-261 Spencer Street DOCKLANDS 3008   
 2  Shop 102, Level 1, 163-261 Spencer Street DOCKLANDS 3008   
 
    industry_anzsic4_code        industry_anzsic4_description   longitude  \
 0                      0                        Vacant Space  144.950564   
 1                   4244        Newspaper and Book Retailing  144.950564   
 2                   4259  Other Personal Accessory Retailing  144.950564   
 
     latitude                              point  
 0 -37.814509  -37.8145089728263, 144.9505641424  
 1 -37.814509  -37.8145089728263, 144.9505641424  
 2 -37.814509  -37.8145089728263, 144.9505641424  ,
          business_name   latitude   longitude
 0  Transit Rooftop Bar -37.817778  144.969942
 1         Taxi Kitchen -37.817778  144.969942
 2       Taxi Riverside -37.817778  144.969942,
    location_id      sensor_description sensor_name installation_date  \
 0            3       Melbourne Central    Swa295_T        2009-03-25   
 1            5          Princes Bridge     PriNW_T        2009-03-26   
 2            9  Southern Cross Station    Col700_T        2009-03-23   
 
                               note location_type status direction_1  \
 0                              NaN       Outdoor      A       North   
 1  Replace with: 00:6e:02:01:9e:54       Outdoor      A       North   
 2                              NaN       Outdoor      A        East   
 
   direction_2   latitude   longitude                    location  
 0       South -37.811015  144.964295  -37.81101524, 144.96429485  
 1       South -37.818742  144.967877  -37.81874249, 144.96787656  
 2        West -37.819830  144.951026  -37.81982992, 144.95102555  )

Data Cleaning and Processing

We merge pedestrian data with sensor locations, create timestamps, and keep only useful fields for weekend rollups.

In [7]:
# Merge coordinates 
ped_df = ped_df.merge(
    loc_df[['sensor_name','latitude','longitude','sensor_description']], 
    on='sensor_name', how='left'
)

# Standardise column names and build datetime
ped_df = ped_df.rename(columns={
    'sensing_date': 'date',
    'hourday': 'hour',
    'pedestriancount': 'hourly_count',
    'sensor_description': 'location_name'   
})

ped_df['date'] = pd.to_datetime(ped_df['date'], errors='coerce')
ped_df['hour'] = pd.to_numeric(ped_df['hour'], errors='coerce')
ped_df['date_time'] = pd.to_datetime(
    ped_df['date'].astype(str) + ' ' + ped_df['hour'].astype('Int64').astype(str) + ':00',
    errors='coerce'
)

# Clean coordinate fields
ped_df['latitude']  = pd.to_numeric(ped_df['latitude'], errors='coerce')
ped_df['longitude'] = pd.to_numeric(ped_df['longitude'], errors='coerce')

# Drop invalid rows
ped_df = ped_df.dropna(subset=['latitude','longitude','date_time','hourly_count']).copy()

print("Ped rows after cleaning:", len(ped_df))
ped_df[['location_name','date_time','hourly_count','latitude','longitude']].head(3)
Ped rows after cleaning: 1486330
Out[7]:
location_name date_time hourly_count latitude longitude
0 Flinders Ln -Degraves St (North) 2024-10-27 15:00:00 445 -37.816848 144.965598
1 231 Bourke St 2024-11-13 20:00:00 388 -37.813331 144.966756
2 231 Bourke St 2025-03-12 13:00:00 845 -37.813331 144.966756

Feature Engineering (Weekend Filter & Aggregation)

We isolate Saturday/Sunday and compute total weekend foot traffic per sensor.

In [8]:
# Create weekday column
ped_df['weekday'] = ped_df['date_time'].dt.day_name()

# Filter for weekends
weekend_df = ped_df[ped_df['weekday'].isin(['Saturday', 'Sunday'])].copy()

# **Aggregate total weekend count per full location name**
weekend_summary = (
    weekend_df
    .groupby(['location_name', 'latitude', 'longitude'], as_index=False)['hourly_count']
    .sum()
    .rename(columns={'hourly_count': 'total_weekend_count'})
    .sort_values('total_weekend_count', ascending=False)
)

# top 10 busiest weekend locations
top10_weekend = (
    weekend_summary
    .head(10)
    .reset_index(drop=True)  
)


top10_weekend.index = top10_weekend.index + 1
top10_weekend.index.name = "Rank"

# Display final ranked table
top10_weekend
Out[8]:
location_name latitude longitude total_weekend_count
Rank
1 Southbank -37.820187 144.965085 7817620
2 Flinders La-Swanston St (West) -37.816686 144.966897 6770532
3 State Library - New -37.810578 144.964443 6296885
4 Elizabeth St - Flinders St (East) - New footpath -37.817980 144.965034 5876190
5 Melbourne Central-Elizabeth St (East) -37.812585 144.962578 4722217
6 Town Hall (West) -37.814880 144.966088 4553044
7 Little Collins St-Swanston St (East) -37.814141 144.966094 4085416
8 Melbourne Central -37.811015 144.964295 4074855
9 Melbourne Convention Exhibition Centre -37.824018 144.956044 4038193
10 The Arts Centre -37.821299 144.968793 3899753

Verify Data Cleaning

A quick look at the top 10 weekend hotspots.

In [9]:
top10_weekend[['location_name','total_weekend_count']].head(10)
Out[9]:
location_name total_weekend_count
Rank
1 Southbank 7817620
2 Flinders La-Swanston St (West) 6770532
3 State Library - New 6296885
4 Elizabeth St - Flinders St (East) - New footpath 5876190
5 Melbourne Central-Elizabeth St (East) 4722217
6 Town Hall (West) 4553044
7 Little Collins St-Swanston St (East) 4085416
8 Melbourne Central 4074855
9 Melbourne Convention Exhibition Centre 4038193
10 The Arts Centre 3899753

Data Subsetting

We will compute two business-oriented metrics around each hotspot (within 200 m): 1) Total nearby businesses (from Business Establishments) 2) Named cafés/restaurants

In [10]:
def haversine_np(lat1, lon1, lat2, lon2):
    """Vectorized Haversine distance in meters."""
    R = 6371000.0
    phi1, phi2 = np.radians(lat1), np.radians(lat2)
    dphi = np.radians(lat2 - lat1)
    dlambda = np.radians(lon2 - lon1)
    a = np.sin(dphi/2.0)**2 + np.cos(phi1)*np.cos(phi2)*np.sin(dlambda/2.0)**2
    return 2*R*np.arcsin(np.sqrt(a))

nearby_business_counts = []
example_cafes = []

for _, s in top10_weekend.iterrows():
    lat0, lon0 = s['latitude'], s['longitude']

    
    biz_box = business_df[
        (business_df['latitude']  >= lat0-0.002) &
        (business_df['latitude']  <= lat0+0.002) &
        (business_df['longitude'] >= lon0-0.002) &
        (business_df['longitude'] <= lon0+0.002)
    ]
    d_biz = haversine_np(lat0, lon0, biz_box['latitude'].values, biz_box['longitude'].values)
    count_biz = int((d_biz <= 200).sum())
    nearby_business_counts.append(count_biz)

    cafe_box = cafes_df[
        (cafes_df['latitude']  >= lat0-0.002) &
        (cafes_df['latitude']  <= lat0+0.002) &
        (cafes_df['longitude'] >= lon0-0.002) &
        (cafes_df['longitude'] <= lon0+0.002)
    ]
    d_cafe = haversine_np(lat0, lon0, cafe_box['latitude'].values, cafe_box['longitude'].values)
    nearby_cafes = cafe_box[d_cafe <= 200]
    names = ', '.join(nearby_cafes['business_name'].dropna().astype(str).head(3).tolist())
    example_cafes.append(names)


top10_weekend['nearby_businesses'] = nearby_business_counts
top10_weekend['example_cafes'] = example_cafes


top10_weekend[['location_name','total_weekend_count','nearby_businesses','example_cafes']]
Out[10]:
location_name total_weekend_count nearby_businesses example_cafes
Rank
1 Southbank 7817620 4292 Miyako Japanese Cuisine & Teppanyaki, Bluetrain, The Deck
2 Flinders La-Swanston St (West) 6770532 20034 RMB Cafe, The Mill House Bar, Il Tempo 2 Pasta
3 State Library - New 6296885 12805 Gong Cha, Harajuku Crepes, Theobroma Chocolate Lounge
4 Elizabeth St - Flinders St (East) - New footpath 5876190 12989 Subway, Mad Mex Flinders Lane, RMB Cafe
5 Melbourne Central-Elizabeth St (East) 4722217 17257 La Belle Miette, Le Petite Creperie, Big Boy BBQ
6 Town Hall (West) 4553044 23571 Health Cosmos, Box On Collins Restaurant, La Vita Buona 'The Good Life' Cellar Bar Deli
7 Little Collins St-Swanston St (East) 4085416 18753 Oh Deer, Salero Kito Padang Melbourne, Sambal Kampung
8 Melbourne Central 4074855 15982 1000 Wat, 8 Bit, Gong Cha
9 Melbourne Convention Exhibition Centre 4038193 170 Crown Plaza, Crown Plaza, Crown Plaza
10 The Arts Centre 3899753 516 Mezz Bar, Jarrah Restaurant & Bar, Sake Restaurant & Bar

Data Exploration and Visualisation

Visualisation of Top Weekend Hotspots (Bar Chart)

In [11]:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

# Calculate nearby cafés count for each hotspot
cafe_counts = []
for _, sensor in top10_weekend.iterrows():
    subset_cafes = cafes_df[
        (cafes_df['latitude']  >= sensor['latitude']-0.002) &
        (cafes_df['latitude']  <= sensor['latitude']+0.002) &
        (cafes_df['longitude'] >= sensor['longitude']-0.002) &
        (cafes_df['longitude'] <= sensor['longitude']+0.002)
    ]
    distances = haversine_np(sensor['latitude'], sensor['longitude'],
                             subset_cafes['latitude'].values, subset_cafes['longitude'].values)
    cafe_counts.append((distances <= 200).sum())

top10_weekend['nearby_cafes'] = cafe_counts

#  Create 3 subplots (horizontal bars) 
fig, axes = plt.subplots(1, 3, figsize=(22, 10), sharey=True)

#  Foot traffic
axes[0].barh(top10_weekend['location_name'], top10_weekend['total_weekend_count'], color='steelblue')
axes[0].set_title("Top 10 Weekend Pedestrian Hotspots", fontsize=14)
axes[0].set_xlabel("Total Weekend Foot Traffic")
axes[0].xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
axes[0].invert_yaxis()
for i, v in enumerate(top10_weekend['total_weekend_count']):
    axes[0].text(v + 50000, i, f"{v:,}", va='center', fontsize=8, color='black')

#  Nearby businesses
axes[1].barh(top10_weekend['location_name'], top10_weekend['nearby_businesses'], color='orange')
axes[1].set_title("Nearby Businesses within 200m", fontsize=14)
axes[1].set_xlabel("Number of Nearby Businesses")
axes[1].xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
axes[1].invert_yaxis()
for i, v in enumerate(top10_weekend['nearby_businesses']):
    axes[1].text(v + 50, i, f"{v:,}", va='center', fontsize=8, color='black')

#  Nearby cafés
axes[2].barh(top10_weekend['location_name'], top10_weekend['nearby_cafes'], color='green')
axes[2].set_title("Nearby Cafés/Restaurants within 200m", fontsize=14)
axes[2].set_xlabel("Number of Nearby Cafés")
axes[2].xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
axes[2].invert_yaxis()
for i, v in enumerate(top10_weekend['nearby_cafes']):
    axes[2].text(v + 5, i, f"{v:,}", va='center', fontsize=8, color='black')


plt.subplots_adjust(wspace=0.4)
plt.tight_layout()
plt.show()
No description has been provided for this image

Weekend vs Weekday Comparison

To highlight the importance of weekend activity, comparing total pedestrian counts at the busiest five locations between weekdays and weekends. This helps show how weekend demand differs from normal weekdays.

In [15]:
# Add day_type column 
ped_df['day_of_week'] = ped_df['date_time'].dt.day_name()
ped_df['day_type'] = ped_df['day_of_week'].apply(
    lambda x: "Weekend" if x in ["Saturday","Sunday"] else "Weekday"
)

# Group by descriptive location and day_type 
compare_avg = ped_df.groupby(["location_name","day_type"])["hourly_count"].mean().unstack()

#  Top 5 busiest locations 
compare_top5_avg = compare_avg.loc[
    compare_avg.sum(axis=1).sort_values(ascending=False).head(5).index
]


compare_top5_avg.plot(
    kind="bar",
    figsize=(10,6),
    color=["#1f77b4", "#ff7f0e"]  
)

plt.title("Average Daily Pedestrian Counts: Weekend vs Weekday (Top 5 Locations)", fontsize=14)
plt.ylabel("Average Pedestrian Count per Day")
plt.xlabel("Location")
plt.xticks(rotation=45, ha="right")
plt.legend(title="Day Type")
plt.tight_layout()
plt.show()

#  Print percentage differences with names 
percent_diff = (
    (compare_top5_avg["Weekend"] - compare_top5_avg["Weekday"])
    / compare_top5_avg["Weekday"]
) * 100

print("\n--- Weekend vs Weekday % Difference (Top 5 Locations) ---\n")
for loc, diff in percent_diff.items():
    if diff > 0:
        print(f"{loc}: +{diff:.1f}% higher on weekends")
    else:
        print(f"{loc}: {diff:.1f}% lower on weekends")
No description has been provided for this image
--- Weekend vs Weekday % Difference (Top 5 Locations) ---

Flinders La-Swanston St (West): +6.9% higher on weekends
Southbank: +14.2% higher on weekends
Elizabeth St - Flinders St (East) - New footpath: -5.8% lower on weekends
368 Elizabeth Street: +2.3% higher on weekends
State Library - New: +9.1% higher on weekends

Business Composition in Busiest Weekend Location

In [16]:
#  Get busiest location details
busiest_lat = top10_weekend.iloc[0]['latitude']
busiest_lon = top10_weekend.iloc[0]['longitude']
busiest_name = top10_weekend.iloc[0]['location_name']   

#  Filter all businesses within 200m of this location
distances_all = haversine_np(
    busiest_lat, busiest_lon,
    business_df['latitude'].values,
    business_df['longitude'].values
)
nearby_all_businesses = business_df[distances_all <= 200]

#  Load cafés dataset if not already loaded
CAFES_DATASET = 'cafes-and-restaurants-with-seating-capacity'
cafes_df = fetch_dataset_csv(BASE_URL, CAFES_DATASET, API_KEY)

# Convert coordinates to numeric for cafés dataset
cafes_df['latitude'] = pd.to_numeric(cafes_df['latitude'], errors='coerce')
cafes_df['longitude'] = pd.to_numeric(cafes_df['longitude'], errors='coerce')
cafes_df = cafes_df.dropna(subset=['latitude', 'longitude'])

#  Filter cafes/restaurants within 200m
distances_cafes = haversine_np(
    busiest_lat, busiest_lon,
    cafes_df['latitude'].values,
    cafes_df['longitude'].values
)
nearby_cafes = cafes_df[distances_cafes <= 200]

#  Prepare counts for pie chart
cafe_count = len(nearby_cafes)
other_count = len(nearby_all_businesses) - cafe_count

#  Plot pie chart
plt.figure(figsize=(6, 6))
plt.pie(
    [cafe_count, other_count],
    labels=['Cafes/Restaurants', 'Other Businesses'],
    autopct='%1.1f%%',
    startangle=140,
    colors=['#ff9999','#66b3ff']
)
plt.title(f"Business Composition within 200m of {busiest_name}")
plt.show()

print(f"{busiest_name} has {cafe_count} cafes/restaurants out of {len(nearby_all_businesses)} total businesses nearby.")
No description has been provided for this image
Southbank has 1157 cafes/restaurants out of 4336 total businesses nearby.

Top 5 Cafes/Restaurants near the busiest location

In [17]:
if 'trading_name' in cafes_df.columns:
    top5_cafes = nearby_cafes[['trading_name', 'seating_type', 'number_of_seats', 'latitude', 'longitude']].head(5)
elif 'business_name' in cafes_df.columns:
    top5_cafes = nearby_cafes[['business_name', 'latitude', 'longitude']].head(5)
else:
    top5_cafes = nearby_cafes.head(5)

print(f"\nTop 5 Cafes/Restaurants within 200m of {busiest_name}:\n")  

for idx, row in top5_cafes.iterrows():
    name = row.get('trading_name', row.get('business_name', 'Unknown'))
    seats = row.get('number_of_seats', 'N/A')
    seat_info = f" – {seats} seats" if seats != 'N/A' else ""
    print(f"- {name}{seat_info}")
Top 5 Cafes/Restaurants within 200m of Southbank:

- Miyako Japanese Cuisine & Teppanyaki – 40 seats
- Bluetrain – 180 seats
- The Deck – 20 seats
- Pure South Dining – 20 seats
- Grill'd – 30 seats

Visualisation of Hotspots and Nearby Business Context (Interactive Map)

Map popups show weekend foot traffic, total businesses within 200 m, and example cafés.

In [18]:
from branca.element import Template, MacroElement
import folium
import os

#  Ensure nearby_businesses column exists 
if 'nearby_businesses' not in top10_weekend.columns:
    nearby_business_counts = []
    for _, s in top10_weekend.iterrows():
        lat0, lon0 = s['latitude'], s['longitude']

        biz_box = business_df[
            (business_df['latitude']  >= lat0-0.002) &
            (business_df['latitude']  <= lat0+0.002) &
            (business_df['longitude'] >= lon0-0.002) &
            (business_df['longitude'] <= lon0+0.002)
        ]
        d_biz = haversine_np(lat0, lon0, biz_box['latitude'].values, biz_box['longitude'].values)
        count_biz = int((d_biz <= 200).sum())
        nearby_business_counts.append(count_biz)

    top10_weekend['nearby_businesses'] = nearby_business_counts

#  Ensure example_cafes column exists 
if 'example_cafes' not in top10_weekend.columns:
    example_cafes = []
    for _, s in top10_weekend.iterrows():
        lat0, lon0 = s['latitude'], s['longitude']
        cafe_box = cafes_df[
            (cafes_df['latitude']  >= lat0-0.002) &
            (cafes_df['latitude']  <= lat0+0.002) &
            (cafes_df['longitude'] >= lon0-0.002) &
            (cafes_df['longitude'] <= lon0+0.002)
        ]
        d_cafe = haversine_np(lat0, lon0, cafe_box['latitude'].values, cafe_box['longitude'].values)
        nearby_cafes = cafe_box[d_cafe <= 200]
        names = ', '.join(nearby_cafes['business_name'].dropna().astype(str).head(3).tolist())
        example_cafes.append(names)
    top10_weekend['example_cafes'] = example_cafes

#  Create map 
melbourne_map2 = folium.Map(location=[-37.8136, 144.9631], zoom_start=13)

for _, row in top10_weekend.iterrows():
    if row['nearby_cafes'] >= 50:
        marker_color = 'darkgreen'
    elif row['nearby_cafes'] >= 20:
        marker_color = 'orange'
    else:
        marker_color = 'lightblue'

    if row['example_cafes'] and row['example_cafes'] != "—":
        li_items = "".join(f"<li>{n}</li>" for n in row['example_cafes'].split(", "))
        cafes_list_html = f"<b>Nearby cafés (≤200m):</b><ul style='margin:4px 0 0 18px;'>{li_items}</ul>"
    else:
        cafes_list_html = "<b>Nearby cafés (≤200m):</b> —"

    popup_text = (
        f"<b>{row['location_name']}</b><br>"
        f"Weekend Foot Traffic: {int(row['total_weekend_count']):,}<br>"
        f"Nearby Businesses (≤200m): {int(row['nearby_businesses']):,}<br>"
        f"Nearby Cafés (≤200m): {int(row['nearby_cafes']):,}<br>"
        f"{cafes_list_html}"
    )

    folium.Marker(
        location=[row['latitude'], row['longitude']],
        popup=popup_text,
        icon=folium.Icon(color=marker_color, icon='briefcase')
    ).add_to(melbourne_map2)


legend_html = """
{% macro html(this, kwargs) %}
<div style="
    position: fixed; bottom: 50px; left: 50px;
    width: 220px; z-index:9999; font-size:14px;
    background-color: white; border:2px solid grey; border-radius:8px; padding: 10px;">
<b>Café Density Legend</b><br>
<i class="fa fa-map-marker fa-2x" style="color:darkgreen"></i> 50+ cafés<br>
<i class="fa fa-map-marker fa-2x" style="color:orange"></i> 20–49 cafés<br>
<i class="fa fa-map-marker fa-2x" style="color:lightblue"></i> <20 cafés
</div>
{% endmacro %}
"""
legend = MacroElement()
legend._template = Template(legend_html)
melbourne_map2.get_root().add_child(legend)

# Show interactive map locally 
melbourne_map2

#  Save interactive HTML 
map_html_file = "Top10_Weekend_Map.html"
melbourne_map2.save(map_html_file)
print(f" Interactive map saved as {map_html_file}")


png_file = "Top10_Weekend_Map.png"
if os.path.exists(png_file):
    from IPython.display import Image, display
    display(Image(png_file))
    print("🖼️ Static PNG preview displayed (for GitHub).")
else:
    print("⚠️ No static PNG found. If needed, open the HTML map in a browser, take a screenshot, save as Top10_Weekend_Map.png, and commit it.")
    
âś… Interactive map saved as Top10_Weekend_Map.html
No description has been provided for this image
🖼️ Static PNG preview displayed (for GitHub).

📍 Weekend Business Activity Map¶

Static preview is shown below (for GitHub).
➡️ Click here to view the full interactive map

Business-First Insights

We summarise the key weekend hotspots with business presence for decision-making.

In [19]:
def fmt_int(x):
    try:
        return f"{int(x):,}"
    except:
        return str(x)

print("\n--- Weekend Business Activity Insights ---\n")

# Busiest location
busiest = top10_weekend.iloc[0]
print(f"- Busiest weekend location: {busiest['location_name']} "
      f"with {fmt_int(busiest['total_weekend_count'])} people.")

# Top 3 locations
print("\n- Top 3 weekend hotspots (with nearby businesses & café examples):")
for i in range(min(3, len(top10_weekend))):
    r = top10_weekend.iloc[i]
    cafes = r.get('example_cafes', '—') or '—'
    print(f"  {i+1}. {r['location_name']} – {fmt_int(r['total_weekend_count'])} people; "
          f"{fmt_int(r.get('nearby_businesses', 0))} businesses; Cafés: {cafes}")

# Recommendations
print("\n- Recommendations:")
print("  • Extend weekend opening hours in identified hotspots.")
print("  • Run targeted weekend promotions where both footfall and business presence are high.")
print("  • Consider pop-ups/events within ≤200 m of top hotspots to capture leisure traffic.")
--- Weekend Business Activity Insights ---

- Busiest weekend location: Southbank with 7,817,620 people.

- Top 3 weekend hotspots (with nearby businesses & café examples):
  1. Southbank – 7,817,620 people; 4,292 businesses; Cafés: Miyako Japanese Cuisine & Teppanyaki, Bluetrain, The Deck
  2. Flinders La-Swanston St (West) – 6,770,532 people; 20,034 businesses; Cafés: RMB Cafe, The Mill House Bar, Il Tempo 2 Pasta
  3. State Library - New – 6,296,885 people; 12,805 businesses; Cafés: Gong Cha, Harajuku Crepes, Theobroma Chocolate Lounge

- Recommendations:
  • Extend weekend opening hours in identified hotspots.
  • Run targeted weekend promotions where both footfall and business presence are high.
  • Consider pop-ups/events within ≤200 m of top hotspots to capture leisure traffic.

Conclusion

This business-first analysis connects weekend pedestrian demand with nearby business supply. It highlights where the city is busiest on weekends and what commercial presence exists around those locations, supporting decisions on rostering, promotions, and event activation.

Observations

Top weekend hotspots cluster around well-known CBD corridors where business density is high. Named cafés near hotspots provide tangible examples for campaign or partnership targeting. Some high-footfall areas have fewer nearby businesses, indicating potential gaps/opportunities.

Recommendations

Prioritise weekend staffing and extended hours around the busiest sensors. Activate local marketing (offers, street promotions) within ≤200 m of top hotspots. Partner with nearby cafés for co-promotions during peak weekend hours. Trial pop-ups in high-footfall zones with relatively lower business counts to test demand. Limitations

The general Business Establishments export here does not include names; we therefore added a cafés dataset to provide named examples. The 200 m radius is a practical default; results will vary with smaller/larger radii. Sensor counts reflect the immediate area; micro-location effects (laneways, arcades) may differ. Future Work

Add other named industry datasets (e.g., bars/pubs, retail outlets) for richer examples. Segment weekend time-of-day patterns (morning vs evening peaks). Test 100 m/300 m radii and compare business impact metrics.

Export

Save the final ranked hotspots with business counts and example cafés for reporting.

In [20]:
out = top10_weekend[['location_name','latitude','longitude',
                     'total_weekend_count','nearby_businesses','example_cafes']].copy()

out.to_csv("Top10_Weekend_BusinessImpact.csv", index=False)
print("Saved:", "Top10_Weekend_BusinessImpact.csv")
Saved: Top10_Weekend_BusinessImpact.csv
In [21]:
import os
print(os.path.abspath("Top10_Weekend_Map.html"))
/Users/pinnamsahithi/Desktop/Top10_Weekend_Map.html